/*
 * Copyright (c) 2012 - 2013 Johann Hanne
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "ethernet.h"
#include "delay.h"

static uint8_t mac_address[6] = {
    0x00, 0x01, 0x02, 0x03, 0x04, 0x05
};

/* For some weird reason, using this simple function crashes the CPU?! */
#if 0
static void ethernet_phy_powerup(void) {
    iow_clearbits(0xf66, 0x0080); /* Set output mode */
    iow_setbits(0xf64, 0x0080); /* Set port high */
}
#else
static void ethernet_phy_powerup(void);
#pragma aux ethernet_phy_powerup = ""  \
    "mov dx, 0xf66" \
    "in ax, dx" \
    "and ax, 0xff7f" \
    "out dx, ax" \
    "mov dx, 0xf64" \
    "in ax, dx" \
    "or ax, 0x0080" \
    "out dx, ax" \
    modify [ax dx];
#endif

static void ethernet_reset(void) {
    uint8_t regval;

    ethernet_register_write(0x00, 0x01);

    do {
        regval = ethernet_register_read(0x00);
    } while ((regval & 0x01) != 0x00);
}

void ethernet_set_mac_address(const uint8_t *mac) {
    if (mac != NULL) {
        memcpy(mac_address, mac, 6);
    }

    ethernet_register_write(0x10, mac_address[0]);
    ethernet_register_write(0x11, mac_address[1]);
    ethernet_register_write(0x12, mac_address[2]);
    ethernet_register_write(0x13, mac_address[3]);
    ethernet_register_write(0x14, mac_address[4]);
    ethernet_register_write(0x15, mac_address[5]);
}

void ethernet_get_mac_address(uint8_t *mac) {
    memcpy(mac, mac_address, 6);
}

static void ethernet_setup_rx_descriptors(void) {
    struct ethernet_rx_descriptor *ethdr;
    uint32_t dataaddr;
    int i;

    ethernet_register_write(0x36,
        (uint8_t)( SEGADDR_TO_PHYSADDR(ETHERNET_RX_DESCRIPTOR_RING_ADDRESS) & 0x000000ff       ));
    ethernet_register_write(0x37,
        (uint8_t)((SEGADDR_TO_PHYSADDR(ETHERNET_RX_DESCRIPTOR_RING_ADDRESS) & 0x0000ff00) >>  8));
    ethernet_register_write(0x38,
        (uint8_t)((SEGADDR_TO_PHYSADDR(ETHERNET_RX_DESCRIPTOR_RING_ADDRESS) & 0x00ff0000) >> 16));

    ethdr = (struct ethernet_rx_descriptor *)ETHERNET_RX_DESCRIPTOR_RING_ADDRESS;
    dataaddr = SEGADDR_TO_PHYSADDR(ETHERNET_RX_DATA_BASE_ADDRESS);

    /* Use a 16 entry descriptor loop for RX */
    for (i = 0; i < 16; i++) {
        ethdr->flags = 0x0080;
        ethdr->rxlen = 0;
        ethdr->unknown1 = 0;
        ethdr->unknown2 = 0;
        ethdr->next = 1;
        ethdr->unknown3 = 0;
        ethdr->dataaddr = toggle_endianness_uint32(dataaddr);

        ethdr++;
        dataaddr += 2048;
    }

    /* Let the last descriptor's "next" field point to the first descriptor
       so that we get a descriptor ring */
    ethdr--;
    ethdr->next = 0x8f;
}

static void ethernet_setup_tx_descriptors(void) {
    struct ethernet_tx_descriptor *ethdr;
    int i;

    ethernet_register_write(0x32,
        (uint8_t)( SEGADDR_TO_PHYSADDR(ETHERNET_TX_DESCRIPTOR_RING_ADDRESS) & 0x000000ff       ));
    ethernet_register_write(0x33,
        (uint8_t)((SEGADDR_TO_PHYSADDR(ETHERNET_TX_DESCRIPTOR_RING_ADDRESS) & 0x0000ff00) >>  8));
    ethernet_register_write(0x34,
        (uint8_t)((SEGADDR_TO_PHYSADDR(ETHERNET_TX_DESCRIPTOR_RING_ADDRESS) & 0x00ff0000) >> 16));

    ethdr = (struct ethernet_tx_descriptor *)ETHERNET_TX_DESCRIPTOR_RING_ADDRESS;

    /* Use a 16 entry descriptor loop for TX
       (will be adjusted for every TX operation) */
    for (i = 0; i < 16; i++) {
        ethdr->flags = 0x0000;
        ethdr->txlen1 = 0;
        ethdr->txlen2 = 0;
        ethdr->unknown1 = 0;
        ethdr->next = 1;
        ethdr->dataaddr1 = 0;
        ethdr->dataaddr2 = 0;

        ethdr++;
    }

    /* Let the last descriptor's "next" field point to the first descriptor
       so that we get a descriptor ring */
    ethdr--;
    ethdr->next = 0x8f;
}

/*
 * This is basically an optimized variant of
 *   ethdr->dataaddr1 = toggle_endianness_uint32(SEGADDR_TO_PHYSADDR(data1));
 *   ethdr->txlen1 = toggle_endianness_uint16(len1);
 * It won't work with addresses 0xffXX:YYff, where XX + YY > 0xff,
 * because an overflow in "adc dh, 0" won't be handled (but this would
 * be outside the 16 MB memory space anyway!)
 */
void set_ethdr_data1(struct ethernet_tx_descriptor *ethdr, uint8_t *data1, size_t len1);
#pragma aux set_ethdr_data1 = ""  \
    "add ah, dl" \
    "adc dh, 0" \
    "xor dl, dl" \
    "xchg al, ah" \
    "mov word ptr es:0x08[bx], dx" \
    "mov word ptr es:0x0a[bx], ax" \
    "xchg cl, ch" \
    "mov word ptr es:0x02[bx], cx" \
    parm [es bx] [dx ax] [cx] modify [dx ax];

void set_ethdr_data2(struct ethernet_tx_descriptor *ethdr, uint8_t *data2, size_t len2);
#pragma aux set_ethdr_data2 = ""  \
    "add ah, dl" \
    "adc dh, 0" \
    "xor dl, dl" \
    "xchg al, ah" \
    "mov word ptr es:0x0c[bx], dx" \
    "mov word ptr es:0x0e[bx], ax" \
    "xchg cl, ch" \
    "mov word ptr es:0x04[bx], cx" \
    parm [es bx] [dx ax] [cx] modify [dx ax];

static void wait_until_bit_80_zero(uint8_t *ptr);
#pragma aux wait_until_bit_80_zero = "" \
    "_loop:" \
    "test byte ptr es:[bx],0x80" \
    "jne _loop" \
    parm [es bx];

void ethernet_tx_direct(uint8_t *data1, size_t len1, uint8_t *data2, size_t len2) {
    struct ethernet_tx_descriptor *ethdr;

    ethdr = (struct ethernet_tx_descriptor *)ETHERNET_TX_DESCRIPTOR_RING_ADDRESS;

    wait_until_bit_80_zero((uint8_t *)ethdr);

    set_ethdr_data1(ethdr, data1, len1);
    set_ethdr_data2(ethdr, data2, len2);

    ethdr->unknown1 = 0;
    ethdr->next = 0;
    ethdr->flags = 0x0080;

    ethernet_register_write(0x30, 0x1);
}

/*
 * Maximum for numelems is 16!
 */
void ethernet_tx_direct_vector(struct ethernet_tx_element *elems, size_t numelems) {
    struct ethernet_tx_descriptor *ethdr;
    size_t c;

    ethdr = (struct ethernet_tx_descriptor *)ETHERNET_TX_DESCRIPTOR_RING_ADDRESS;

    if (numelems > 16) {
        numelems = 16;
    }

    numelems--;

    for (c = 0; c < numelems; c++) {
        wait_until_bit_80_zero((uint8_t *)&ethdr[c]);

        set_ethdr_data1(&ethdr[c], elems[c].data1, elems[c].len1);
        set_ethdr_data2(&ethdr[c], elems[c].data2, elems[c].len2);

        ethdr[c].unknown1 = 0;
        ethdr[c].next = 1;
        ethdr[c].flags = 0x0080;

        ethernet_register_write(0x30, 0x1);
    }

    wait_until_bit_80_zero((uint8_t *)&ethdr[numelems]);

    set_ethdr_data1(&ethdr[numelems], elems[numelems].data1, elems[numelems].len1);
    set_ethdr_data2(&ethdr[numelems], elems[numelems].data2, elems[numelems].len2);

    ethdr[numelems].unknown1 = 0;
    ethdr[numelems].next = numelems | 0x80;
    ethdr[numelems].flags = 0x0080;

    ethernet_register_write(0x30, 0x1);
}

static void tx(size_t len) {
    struct ethernet_tx_descriptor *ethdr;

    ethdr = (struct ethernet_tx_descriptor *)ETHERNET_TX_DESCRIPTOR_RING_ADDRESS;

    ethdr->txlen1 = 0;
    ethdr->dataaddr1 = 0;
    set_ethdr_data2(ethdr, ETHERNET_TX_DATA_BASE_ADDRESS, len);

    ethdr->unknown1 = 0;
    ethdr->next = 0;
    ethdr->flags = 0x0080;

    ethernet_register_write(0x30, 0x1);
}

void ethernet_tx(uint8_t *data, size_t len) {
    wait_until_bit_80_zero((uint8_t *)ETHERNET_TX_DESCRIPTOR_RING_ADDRESS);
    memcpy(ETHERNET_TX_DATA_BASE_ADDRESS, data, len);
    tx(len);
}

/* A little crude, but works without pulling in the
   lwip struct pbuf definition... */
void ethernet_tx_linked_list(void *item, size_t offset_payloadptr, size_t offset_length, size_t offset_nextptr) {
    uint8_t *txptr;
    uint16_t len, totallen;

    txptr = (uint8_t *)ETHERNET_TX_DATA_BASE_ADDRESS;
    totallen = 0;

    wait_until_bit_80_zero((uint8_t *)ETHERNET_TX_DESCRIPTOR_RING_ADDRESS);

    for (; item != NULL; item = *(void **)((uint8_t *)item + offset_nextptr)) {
        len = *(uint16_t *)((uint8_t *)item + offset_length);
        memcpy(txptr, *(void **)((uint8_t *)item + offset_payloadptr), len);
        txptr += len;
        totallen += len;
    }

    tx((size_t)totallen);
}

void ethernet_rx(void (*rx_cb)(uint8_t *data, size_t len)) {
    static int cur_rx_index = 0;
    volatile struct ethernet_rx_descriptor *volatile ethdr;
    uint8_t *data;
    size_t len;

    ethdr = (struct ethernet_rx_descriptor *)ETHERNET_RX_DESCRIPTOR_RING_ADDRESS + cur_rx_index;

    while ((ethdr->flags & 0x0080) == 0) {
        data = PHYSADDR_TO_SEGADDR(toggle_endianness_uint32(ethdr->dataaddr));
        len = toggle_endianness_uint16(ethdr->rxlen);
        rx_cb(data, len);

        ethdr->flags = 0x0080;
        ethdr->rxlen = 0;
        cur_rx_index++;
        if (cur_rx_index < 16) {
            ethdr++;
        } else {
            cur_rx_index = 0;
            ethdr = (struct ethernet_rx_descriptor *)ETHERNET_RX_DESCRIPTOR_RING_ADDRESS;
        }
    }
}

int ethernet_init(void) {
    uint8_t reg;

    iow_setbits(0x844, 0x4000);

    ethernet_phy_powerup();
    delay_ms(500);

    iow_setbits(0x842, 0x0100);

    ethernet_reset();

    ethernet_set_mac_address(NULL);

    ethernet_setup_rx_descriptors();
    ethernet_setup_tx_descriptors();

    /* Enable frame reception */
    ethernet_register_write(0x3, 0x31);
    ethernet_register_write(0x31, 0x1);

    /* Enable frame transmission */
    ethernet_register_write(0x1, 0x1);

    return 0;
}
